package joc.internal.transformer;

import java.lang.annotation.Annotation;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javassist.ByteArrayClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.LoaderClassPath;
import javassist.NotFoundException;
import joc.JOCEnabled;
import joc.contract.ContainsStringContract;
import joc.contract.DenyContract;
import joc.contract.GreaterThanLongContract;
import joc.contract.NotNullContract;
import joc.contract.ProfileContract;
import joc.customize.Contract;

import org.apache.log4j.Logger;

public class Transformer implements ClassFileTransformer {

	private Logger logger = Logger.getLogger(Transformer.class);
	private ClassPool pool = ClassPool.getDefault();

	private final static List<Contract> contracts = new ArrayList<Contract>(Arrays.asList(new ProfileContract(),
			new DenyContract(), new GreaterThanLongContract(), new NotNullContract(), new ContainsStringContract()));
	private final static Map<Class<? extends Annotation>, Contract> contractMap = new HashMap<Class<? extends Annotation>, Contract>();

	private final List<ClassTransformer> transformers = new ArrayList<ClassTransformer>(Arrays.asList(
			new CustomPostHookBehaviorTransformer(pool), new CustomPreHookBehaviorTransformer(pool),
			new CustomInvariantClassTransformer(pool), new CustomPreConditionBehaviorTransformer(pool),
			new PostConditionMethodTransformer(pool), new CustomPostConditionBehaviorTransformer(pool),
			new ParameterPreConditionBehaviorTransformer(pool), new FieldInvariantTransformer(pool),
			new GenericBehaviorTransformer(pool)));

	private static Transformer lastTransformer;

	public static Transformer getLastTransformer() {
		return lastTransformer;
	}

	static {
		for (Contract contract : contracts) {
			addContractToMap(contract);
		}
	}

	public ClassPool getPool() {
		return pool;
	}

	private static void addContractToMap(Contract contract) {
		contractMap.put(contract.getAnnotationClass(), contract);
	}

	public static void addContract(Contract contract) {
		contracts.add(contract);
		addContractToMap(contract);
	}

	public static void removeContract(Contract contract) {
		contracts.remove(contract);
		contractMap.remove(contract.getAnnotationClass());
	}

	protected static List<Contract> getContracts() {
		return contracts;
	}

	public static Map<Class<? extends Annotation>, Contract> getContractMap() {
		return contractMap;
	}

	public Transformer(Instrumentation inst) {
		lastTransformer = this;
	}

	@Override
	public byte[] transform(ClassLoader loader, String classNameWithSlashes, Class<?> classBeingRedefined,
			ProtectionDomain protectionDomain, byte[] classfileBuffer) {
		String className = classNameWithSlashes.replace('/', '.');
		logger.debug("transformation started for class " + className);
		try {
			if (loader != null) {
				pool.insertClassPath(new LoaderClassPath(loader));
			}
			if (classfileBuffer != null) {
				pool.insertClassPath(new ByteArrayClassPath(className, classfileBuffer));
			}

			CtClass targetClass = pool.get(className);
			if (targetClass.isInterface()) {
				logger.debug("transformation aborted, as class is an interface");
				return null;
			}
			if (!targetClass.hasAnnotation(JOCEnabled.class)) {
				logger.debug("transformation aborted, as class does not annotate JOCEnabled");
				return null;
			}
			logger.info("transforming class " + className);
			return transformClass(targetClass);
		} catch (NotFoundException e) {
			logger.warn("transformation failed for class '" + className + "': could not load class", e);
		} catch (JOCTransformationException e) {
			logger.warn(e.getMessage(), e);
		} catch (Throwable e) {
			logger.warn("transformation failed for class '" + className + "': unknown error", e);
		}
		return null;
	}

	private byte[] transformClass(CtClass targetClass) throws Exception {
		for (ClassTransformer transformer : transformers) {
			transformer.transform(targetClass);
		}
		return targetClass.toBytecode();
	}

	public Logger getLogger() {
		return logger;
	}

	public void setLogger(Logger logger) {
		this.logger = logger;
	}

	protected void setPool(ClassPool pool) {
		this.pool = pool;
	}

}
